Skip to content

tools/qemu-wasm: PebbleOS emulator in the browser (QEMU→wasm, app install, PebbleKit JS) - #1910

Draft
ericmigi wants to merge 30 commits into
mainfrom
claude/pebbleos-agent-swarm-iviybe
Draft

tools/qemu-wasm: PebbleOS emulator in the browser (QEMU→wasm, app install, PebbleKit JS)#1910
ericmigi wants to merge 30 commits into
mainfrom
claude/pebbleos-agent-swarm-iviybe

Conversation

@ericmigi

Copy link
Copy Markdown
Collaborator

Summary

Adds tools/qemu-wasm/: a browser-based PebbleOS emulator built on the coredevices/qemu generic Pebble machines compiled to WebAssembly, plus the full toolchain and web shell around it. Live at https://ericmigi.github.io/pebble-qemu-wasm/. Everything is contained under tools/qemu-wasm/ — no firmware or build-system changes.

What's in here, layer by layer:

  • Web shell (web/): boots pebble-emery with release firmware, canvas display via shared-memory framebuffer exports, buttons via an atomics-polled bitmask, serial console.
  • Browser "phone" (web/pebble-transport.js, app-install.js, pbw.js, store.js): a JS implementation of the phone side of the Pebble Protocol — QemuProtocol framing over a shared-memory UART bridge, phone-version handshake, and the full 4.x app-install flow (BlobDB → AppFetch → PutBytes with the legacy defective CRC, verified against the firmware's test vectors). Users install apps by pasting an apps.repebble.com link, uploading, or drag-dropping a .pbw.
  • PebbleKit JS (web/pkjs-runtime.js, appmessage.js): apps with pebble-js-app.js get their JS run in a sandboxed iframe with a Pebble shim, wired to the watch over AppMessage (endpoint 0x30), lifecycle driven by app_run_state notifications. Includes config-page support via the return_to convention and an optional CORS-proxy fallback (pkjs-proxy/, a Cloudflare Worker).
  • Performance (patches/, jit/): TCI-interpreter patches (RAM-backed framebuffer, inline TLB fast path, ASYNCIFY_REMOVE lists) take scroll fps from 2.1 → 4.5; the TCG→wasm JIT overlay (ktock/qemu-wasm tree, QEMU 10.2 port of the pebble device set) reaches ~17 fps and 15 s boots. The JIT overlay documents the 10.1→10.2 API drift and should be retired when the wasm TCG backend lands upstream.
  • Deploy bundle (site-dist/): prebuilt gzipped build + apply.sh for the GitHub Pages site. ~3.9 MB of binaries — flagging for maintainer judgment; happy to move these to release assets or CI artifacts in a follow-up if binaries in-tree are unwanted.

Verification

  • Headless node harness: boots to watchface, installs an SDK-built .pbw (BlobDB ack → AppFetch round trip → CRC-clean PutBytes → app fetch result 0), C↔JS AppMessage round trip both directions.
  • Chromium against the exact deployed files (COOP/COEP via coi-serviceworker): same install + a PebbleKit JS app whose XHR result renders on the emulated display.
  • Test apps were built from the v4.35.0 tag's own generated SDK against the v4.35.0 release firmware images.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M


Generated by Claude Code

Eric Migicovsky and others added 15 commits August 21, 2026 20:39
Resurrect the PebbleOS-in-the-browser emulator on top of main's QEMU
instead of the ericmigi/pebble-qemu-wasm STM32-model port it derives
from. coredevices/qemu pebble-10.1 (10.1.5-pebble17) builds for
emscripten out of the box, and the generic virtual machines
(pebble-emery/flint/gabbro) boot the qemu_* release flash images
directly, so the browser build now tracks mainline firmware rather
than a frozen SDK image.

The shell renders the display by polling shared-memory exports and
injects buttons through an atomics bitmask, both provided by a small
emscripten-only bridge in the QEMU tree (patches/0001, to be
upstreamed to coredevices/qemu): a virtual-clock timer drives console
updates under -display none and the page blits the rendered surface,
so no SDL/proxying machinery is involved.

Included: reproducible native build script (emsdk 3.1.50 + wasm32
deps), release-firmware fetcher, COOP/COEP dev server, and a node
headless smoke test. Measured on 4 cores under node: serial banner at
~3 s, launcher + 'Ready for communication.' at ~55 s, 200x228 frames
rendering throughout (TCI interpreter, no JIT yet).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
Add an assembler that packs QEMU wasm, runtime JS, pthread worker, and
the release firmware into one self-contained HTML page (gzip + base64,
unpacked in-browser via DecompressionStream), with an environment probe
that reports exactly which host capability is missing (COOP/COEP,
SharedArrayBuffer, blob workers) instead of hanging on an incompatible
host. Includes a headless chromium test driving both the boot and the
diagnostics paths.

To fit single-page size limits, document the Pebble-only QEMU build:
--without-default-devices --with-devices-arm=pebble with a one-line
devices config, which drops every non-Pebble ARM board and shrinks the
wasm from 39 MB to 25 MB (13.8 MB page total). That surfaced a missing
'select I2C' in the PEBBLE Kconfig entry, now part of the refreshed
browser-glue patch.

Verified: the assembled page boots v4.35.0 emery firmware under
chromium with pixels on canvas, and renders the correct diagnostic when
served without cross-origin isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
Transport-only directory (see its README): the assembled emulator site
for ericmigi/pebble-qemu-wasm's GitHub Pages, so a session with push
access there can deploy without redoing the wasm build. Verified under
a headerless static host with coi-serviceworker providing isolation:
crossOriginIsolated true, firmware boots, display renders.

Remove before merging this branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
Fold the measured wasm performance work into the browser-glue patch:

- pebble_display framebuffer becomes plain RAM (memory_region_init_ram_ptr)
  instead of trapping MMIO: guest pixel writes were tens of thousands of
  interpreted slow-path exits per frame. The CTRL_UPDATE doorbell is the
  only sync point the firmware relies on, so nothing needs to trap.
- inline TLB fast path in the TCI interpreter's tci_qemu_ld/st (ported
  from the ericmigi/pebble-qemu-wasm work): on a TLB hit with no flags,
  access host memory directly instead of the helper_*_mmu call chain.
- skip cpu_io_recompile under emscripten (longjmp is a JS exception).
- -sASYNCIFY_REMOVE for the interpreter hot path, -Doptimization=3,
  mimalloc.

Continuous launcher scroll on pebble-emery v4.35.0 under node
(4 shared cores): 2.1 -> 4.5 fps; boot to 'Ready for communication'
91 s -> 38 s; boot animation 1.3 -> 6.9 fps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
The TCI interpreter plateaus at ~4.5 fps on continuous launcher
scrolling. Port the generic-machine device set onto ktock/qemu-wasm
branch wasm64-tcg-b (QEMU 10.2.50 with the TCG WebAssembly JIT
backend), which compiles hot translation blocks to native wasm:

  boot to 'Ready for communication'  91 s -> 38 s -> 15 s
  continuous launcher scroll        2.1 -> 4.5 -> 16.9 fps
  (baseline TCI -> patched TCI -> JIT, node on 4 shared cores)

tools/qemu-wasm/jit/ carries the overlay (device set ported to the
QEMU 10.2 APIs: hw/core header moves, CharBackend->CharFrontend,
machines must declare arm_machine_interfaces to be visible in the
single-binary machine registry, pebble_audio rewritten as a
register-compatible drain-only stub since the audio subsystem was
rewritten, pebble_set_button_state moved into pebble_gpio.c), the
meson/Kconfig wiring patch, the MEMORY64=2 dependency build script,
and a README with the build recipe and the API-drift map.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
Under the JIT build (-sMEMORY64=2 with WASM_BIGINT) pointer-returning
glue exports arrive in JS as BigInt, and the blit path's pointer
arithmetic throws 'Cannot mix BigInt and other types'. Wrap the two
pointer exports in Number(); a no-op for the wasm32 TCI build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
The page imported ./qemu-system-arm.mjs while every deployment lays the
emscripten output down under its original .js name; browsers resolve
module type from MIME, not extension, so standardize on .js.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
Swap the deployable bundle's emulator to the TCG-to-wasm JIT build
(9 MB wasm, no separate worker file, boots to the watchface in ~15 s
at ~17 fps scrolling) and carry the updated shell. Verified booting
under a headerless static host with coi-serviceworker in chromium.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
The pebble-tool channel (UART1, QemuProtocol framing) previously had no
transport in the browser build. Add an emscripten-only bridge inside
pebble_control: pebble_wasm_serial_ctrl() exports a pair of ring
buffers; the page produces host->guest bytes into the rx ring with
Atomics and a 2 ms virtual-clock timer feeds them through the normal
chardev receive path, while all watch-bound writes are mirrored into
the tx ring for the page to drain. Works alongside a real chardev, and
pebble_control is now created even when serial1 has none.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
Implement the phone side of the app-install flow in JS on top of the
UART1 ring bridge:

- pebble-transport.js: QemuProtocol framing (0xFEED/0xBEEF), Pebble
  Protocol stream reassembly in both directions, and the endpoint-17
  version handshake. The response is V3 with capabilities 0xA3 because
  the firmware replaces (not ORs) session capabilities, and losing
  CommSessionRunState would break launch notifications.
- pbw.js: .pbw (zip) reader on the native DecompressionStream with
  platform-directory selection (emery -> basalt -> diorite -> aplite).
- app-install.js: BlobDB INSERT of the 126-byte AppDBEntry (retrying on
  TRY_LATER), app_run_state RUN to trigger the watch's AppFetch
  request, then PutBytes INIT/PUT/COMMIT/INSTALL per object with the
  legacy defective CRC (verified against the firmware's test vectors).
  Legacy struct-version binaries read the uuid at its pre-3.0 offset.
- store.js: resolves apps.repebble.com / apps.rebble.io links via the
  CORS-enabled appstore API and downloads the release pbw.
- index.html: install panel (paste a store link) plus drag-drop of
  .pbw files onto the page, with transfer progress.

Verified headless under node and in Chromium against the JIT build:
BlobDB ack, AppFetch round-trip, CRC-clean PutBytes commits, and the
watch launching the installed watchface (app fetch result 0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
New JIT build with the serial bridge, the install JS modules, and the
updated shell page; apply.sh now copies the module files. Document the
serial bridge and install flow in the README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
File-picker alternative to drag-drop for installing local .pbw files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
Apps with a pebble-js-app.js now get their JS executed when the watch
launches them, wired to the watch over AppMessage:

- appmessage.js: endpoint 0x30 client. PUSH carries the app uuid and a
  little-endian tuple dictionary; ACK/NACK are two bytes echoing the
  transaction id. Sends are serialized through a queue (one PUSH in
  flight, 10 s ACK timeout) and pushes that arrive while the app's JS
  is still starting are buffered and delivered after 'ready', since
  the watch's first outbox send races the run-state notification.
- pkjs-runtime.js: sandbox runner + Pebble shim (addEventListener,
  sendAppMessage, tokens, getActiveWatchInfo, openURL; timeline APIs
  stubbed). Lifecycle follows app_run_state notifications on endpoint
  52. In the browser the sandbox is a hidden same-origin iframe --
  window globals like localStorage are getter-only, so the shim
  bindings are installed with defineProperty -- with per-app-scoped
  storage and an XHR-over-fetch shim that retries failed cross-origin
  requests through a configurable CORS proxy (?pkjs_proxy=).
- config pages: openURL appends the return_to convention pointing at
  config-return.html, which posts the response fragment back to the
  page for the webviewclosed event.
- pbw.js extracts pebble-js-app.js; index.html registers installed
  apps with the runtime and adds a settings button that triggers
  showConfiguration.

Verified headless (node:vm sandbox) and in Chromium: BlobDB install,
watch-initiated AppMessage push delivered to JS, XHR fetch, and the
reply rendered by the C app on the emulated display.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
Cloudflare Worker the emulator page falls back to when a JS app's API
call has no CORS headers. Origin-allowlisted, GET/HEAD/POST only,
credentials stripped both directions, internal hostnames refused.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
Add the runtime modules and config-return page to site-dist and
apply.sh; document the PKJS architecture in the README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
@ericmigi
ericmigi marked this pull request as draft August 23, 2026 04:33
apply.sh now copies pkjs-proxy/ into the target checkout so the worker
source lives alongside the site it serves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18dcb6284e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +253 to +256
const frame = document.createElement('iframe');
frame.style.display = 'none';
frame.setAttribute('allow', 'geolocation *');
document.body.appendChild(frame);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Isolate installed app code from the page origin

When a user installs any third-party PBW containing pebble-js-app.js, this iframe has neither a sandbox attribute nor a separate origin, and the code is later executed with w.eval. Such an app can access parent.document, the emulator's full API, and all localStorage belonging to the hosting origin; replacing the iframe's localStorage property does not restrict access through parent. Run PBW code in a genuinely isolated iframe or worker and expose only the Pebble bridge through messaging.

Useful? React with 👍 / 👎.

console.log('screenshot: ' + shot);
await browser.close();
server.close();
process.exit(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return failure when the browser boot test times out

If the page never reaches the expected display state—for example because QEMU aborts, pixels remain blank, or the loop simply reaches waitS—execution falls through and exits successfully anyway. This makes the documented coi browser test report a green status for broken builds; track whether the success predicate was reached and return nonzero when it was not.

Useful? React with 👍 / 👎.

Comment on lines +67 to +68
flint: ['diorite', 'aplite', 'basalt'],
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the Chalk fallback for Gabbro

When the board selector passes gabbro, the missing map entry makes the fallback list just ['gabbro'], so PBWs containing the compatible chalk/pebble-app.bin are rejected as having no Gabbro binary. The firmware explicitly maps Gabbro to PlatformTypeChalk for pre-4.2 SDKs in src/fw/process_management/pebble_process_md.c:230-250, so the browser should include Chalk in Gabbro's platform preference.

Useful? React with 👍 / 👎.

Comment on lines +6 to +8
EMSDK4=/home/user/emsdk-4
TARGET=/home/user/wasm-deps64
SRC=/home/user/wasm64-src

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make the JIT dependency paths configurable

Running the documented ./build-deps64.sh command under any account other than the original /home/user environment writes outside the caller's home and commonly fails at the initial mkdir with a permission error. Later steps also copy sources from /home/user/wasm-deps, so even changing permissions does not make the build self-contained; derive these paths from $HOME or accept a work directory argument.

Useful? React with 👍 / 👎.

Eric Migicovsky and others added 10 commits August 23, 2026 04:42
Point the proxy fallback at pkjs-proxy.ericmigi.workers.dev and use it
for store lookups and pbw downloads too, so store installs work before
the asset bucket gets a CORS policy. Accept messageKeys as the appKeys
fallback for modern pbws.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
Browsers block window.open on data: URLs, which is how Clay-based apps
deliver their settings pages. Decode the data: URL, rewrite Clay's
default pebblejs://close target to the same-origin return page, and
render the page in a modal iframe; the return page now posts to parent
as well as opener. Hosted config pages keep the popup + return_to
path. Verified with a Clay-style page round-tripping its settings
payload in Chromium.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
JS-side activity (app start, fetch failures, geolocation denials) was
only visible in devtools; mirror it into the page's console panel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
A silent OS-level location denial or a doubly-failed weather fetch was
invisible; wrap the sandbox geolocation methods and log XHR/fetch
failures and proxy fallbacks into the page console.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
Call the top page's geolocation rather than the sandbox iframe's
(removes the permissions-delegation variable), fall back to IP-based
city-level location when the browser provider fails (common when OS
location services are off), and support a ?loc=lat,lon override.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
Touch: pebble_touch.c gains a browser injection path — the page writes
{seq, down, x, y} in display pixels with Atomics and an 8 ms
virtual-clock poll raises the touch IRQ on state changes, mirroring
the input-layer handler. Canvas pointer events drive it; verified in
Chromium by touch-scrolling the launcher.

Audio: the pebble_audio drain stub now mirrors every DATA sample into
a shared int16 ring with rate/volume/playing mirrors; the page drains
it into WebAudio on a 60 ms poll, scheduling buffers at a running
playhead. The context is created on the first canvas touch to satisfy
autoplay policies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
- The 32 MB SPI flash is saved to OPFS when dirty (15 s cadence,
  strided FNV change detection) and restored on the next boot, so
  installed apps and settings survive reloads; PebbleKit JS sources
  are persisted alongside in localStorage. Factory reset wipes both.
- The page boots automatically on load (?noauto restores the button);
  switching boards reloads onto the chosen machine.
- gabbro (getafix_dvt) is a first-class board option with v4.35.0
  firmware in the site bundle.
- dbgserial (UART2) gains a browser input ring behind a wasm-console
  device property, giving the console panel a working prompt.
- Sensors & phone panel injects battery, compass, tap and time-format
  QemuProtocol frames over the existing serial bridge.

Verified in Chromium: autoboot, install -> OPFS save -> reload ->
restore (no re-fetch, JS registry intact), and the dbgserial prompt
answering 'help'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
- pulse-console.js: full PULSEv2 client. Frame layer (0x55 flags, COBS,
  0x55<->0x00 transparency, CRC-32 residue check), PPP control FSMs for
  LCP and the reliable-transport NCP, and a stop-and-wait LAPB client
  for the TRAIN transport. Logs (push 0x5021 port 3, the 29-byte
  <c16sccQH record) render as [time] <lvl> file:line: message -- the
  qemu build is plaintext PULSE, not hashed, so no dictionary is
  needed. The console input line drives the dbgserial prompt over the
  reliable transport (port 0x3e20).
- phone-extras.js: notifications and voice. buildNotification
  serializes a 46-byte SerializedTimelineItemHeader plus attribute and
  action lists (status must be 0, payload_length exact) into a BlobDB
  insert on the notifs db; the default notification carries a Response
  action with canned replies so the Reply -> Voice path works.
  VoiceService accepts dictation session setups on endpoint 11000,
  counts the (silent) Speex frames on 10000, and on stop returns a
  one-sentence transcription from browser speech recognition with a
  typed-prompt fallback, echoing app_initiated flags and app uuid.

Verified in Chromium end to end: parsed log lines, a 'version' prompt
round trip, a notification banner with working action menu, and
Reply -> Voice dictation displaying the browser-provided transcript.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
Auto-boot disabled the dropdown before it could ever be used, making
board switches impossible. The selector now stays enabled; changing it
reloads the page onto the chosen machine, which is the only way to
restart the emulator anyway. Verified in Chromium: switching from a
running emery to gabbro boots the round 260x260 machine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
The global key handler consumed Enter and arrows even while typing in
the install/console inputs, pressing Select on the watch instead of
submitting the field. Ignore watch-key events when focus is in a form
field, and blur buttons after clicks so the next Enter goes back to
the watch rather than re-clicking the button.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
apply.sh listed JS files by name and was never taught about
pulse-console.js and phone-extras.js, so deployed pages imported a
404 and the whole module script (install button included) died
silently. Copy *.js wholesale, and surface module-load failures in
the status line. Verified by running the full page test against an
apply.sh-produced directory rather than a hand-staged one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>

@gmarull gmarull left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sources likely do not belong to this repo, this is massive, not a simple script. Maybe more patches to our qemu fork, and ship with pebbleos sdk as we do for other stuff.

Eric Migicovsky and others added 3 commits August 23, 2026 07:26
Groundwork for building a Pebble app from source in the browser, using
the prebuilt wasm LLVM toolchain (clang.wasm / lld.wasm) from the
pebble-sdk-browser-compile work. That branch validated the toolchain
under Node with the stock Python SDK tools; this is the browser host
layer its README leaves open.

- wasi-run.js: runs a WASI tool against an in-memory filesystem. The
  toolchain modules are wasm32-wasi-threads builds, so they import
  shared linear memory rather than exporting it, and want a
  thread-spawn hook they never call; both are handled here. The
  vendored shim defaults its path-lookup tracing on when constructed
  without options, which is far too noisy and slow for a real compile.
- codegen.js: the waf-generated files, byte-for-byte -- appinfo.auto.c,
  resource_ids.auto.{c,h}, message_keys.auto.{c,h}, pebble_app.ld and
  appinfo.json.
- elfpack.js: objcopy, inject_metadata and the pbw/manifest writers
  ported from the SDK's Python, including the legacy defective CRC.
- jsbundle.js: esbuild stands in for the SDK's webpack step, matching
  it where it matters (shared additions first, message_keys and
  app_package.json virtual modules, src/common on the resolve path).
- appbuilder.js drives the pipeline; appbuild-worker.js hosts it off
  the main thread and caches the toolchain download.

make-sdkpack.sh assembles what the compiler needs beyond the two tool
binaries: SDK headers and libpebble.a, newlib headers, libgcc/libc for
thumb/v7-m, clang's builtin headers, and the SDK's pkjs helper modules.

Validated under Node against a golden waf build of pebble-timer: all 11
sources compile, the link is byte-identical in structure to the waf
one, and the resulting pbw installs and runs in the emulator. In
Chromium, clang.wasm decompresses, compiles and builds an SDK source in
under a second.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
Ports the SDK's waf resource generators to JS so an app's images, fonts
and raw assets can be turned into an app_resources.pbpack client-side:
pbpack.py for the container, bitmapgen.py for PBI, png2pblpng.py plus
pebble_image_routines.py for the palettised PNGs, and font/fontgen.py
for PFO v3. PNG decode and encode are done here rather than through the
canvas so the output is identical under Node and in a worker; zlib comes
from the streams API.

FreeType is not available, so glyph rasterisation is delegated to the
host through a rasterizeGlyph callback -- canvas in the browser, and in
the test the golden font's own glyphs, which isolates the serialiser.

Two SDK behaviours are deliberately reproduced rather than corrected,
because matching waf's output is the point: bitmapgen._set_bbox()
transposes its columns back into rows before computing the right edge,
so empty right-hand columns are never cropped; and a font's table
offsets are relative to the sfnt header, which is not always at the
start of the file (the pebble-timer font carries a 260-byte prefix).

Validated against a golden waf build of pebble-timer on emery: the
pbpack container is byte-exact, 10 of 17 resources are byte-exact
including all four fonts, and the rest -- where the SDK's palette order
comes from a Python set and so cannot be reproduced -- decode to
identical pixels. Building the whole app from a clean checkout now
takes 2.3s under Node, and the resulting pbw runs in the emulator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
Paste a repo holding an SDK app into the emulator page and it compiles
client-side and installs what it built -- no SDK, no toolchain, no
server. Building coredevices/pebble-timer takes about four seconds once
the tools are cached.

Wires the compiler into the page: a repo URL box next to the install
one, a worker that survives across builds, and the codeload zip URLs a
repo (or a /tree/<ref>) maps to. GitHub does not send CORS headers on
codeload, so the fetch falls back to the pkjs proxy already used for
app JS.

The tools themselves (gzipped clang.wasm and lld.wasm, esbuild, and the
emery SDK pack -- 39 MB together) ship in web/tools/ and are cached in
the Cache API after the first build. apply.sh copies them, the page
modules and the vendored shims into a deploy, taking them from web/
rather than site-dist/ so they are not stored twice.

A build saturates every core, which starves QEMU: the watch would fall
far enough behind that the first BlobDB insert timed out and the install
failed outright. Give the emulator a moment before installing, and treat
a silent watch as retryable there rather than fatal.

Also fixes two things found by running it: the worker resolved the tools
directory against its own URL one level down, and Chromium rejects a
font whose sfnt header is not at the start of the file (pebble-timer's
is 260 bytes in) where FreeType accepts it.

Tested in Chromium against a deployed bundle: repo URL to a running app
in 13 seconds, PebbleKit JS included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M
Signed-off-by: Eric Migicovsky <eric@repebble.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants